package claim;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;

import javax.imageio.ImageIO;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.math.stat.descriptive.moment.Mean;
import org.apache.commons.math.stat.descriptive.moment.StandardDeviation;
import org.apache.commons.math.stat.descriptive.rank.Max;
import org.apache.commons.math.stat.descriptive.rank.Min;

import com.vividsolutions.jts.geom.Geometry;

import claim.fuzzy.CFCM;
import claim.fuzzy.ExponentMembershipFunction;
import claim.fuzzy.LinearMembershipFunction;
import claim.fuzzy.MembershipFunction;
import claim.fuzzy.MinusXPlus1ExponentMembershipFunction;
import claim.fuzzy.SqrtMembershipFunction;
import claim.geom.Feature;
import claim.geom.PointFeature;
import claim.geom.PolygonFeature;
import claim.geoobj.Buffer;
import claim.geoobj.ClassConfiguration;
import claim.geoobj.TransitionZone;
import claim.io.ShapeIO;
import claim.io.TextIO;
import claim.layer.CachedFeatureLayer;
import claim.layer.FeatureLayer;
import claim.layer.Layer;

public class ComputeCFCM {

	private static final Log LOG = LogFactory.getLog(Class.class.getName());
	
	private static final NumberFormat CFCM_FORMAT = new DecimalFormat("0.#####");

	private static final String URL_REF = "reference_file";
	private static final String URL_RESULT = "result_file";
	private static final String ATTRIBUTE_REF = "reference_class_attribute";
	private static final String ATTRIBUTE_RESULT = "result_class_attribute";

	private static final String CLASSNAME_TOKEN = "${CLASSNAME}";
	
	private static final String OUTPUT_PATH = "output_path";
	
	private static final String OUTPUT_BUFFERS_REF = "output_buffers_reference";
	private static final String OUTPUT_BUFFERS_RESULT = "output_buffers_result";
	
	private static final String OUTPUT_CFCM_POINTS = "output_cfcm_points";
	private static final String OUTPUT_CFCM_RESULTS = "output_cfcm_results";
	
	private static final String NUM_POINTS_X = "num_points_x";
	private static final String NUM_POINTS_Y = "num_points_y";

	private static final String LOAD_BUFFERS = "load_buffers";

	private Properties props;

	private URL urlRef;

	private URL urlResult;

	private Collection<PolygonFeature> featuresRef;

	private Collection<PolygonFeature> featuresResult;

	private FeatureLayer layerRef;

	private FeatureLayer layerResult;

	private boolean loadBuffers;
	
	private boolean cached = false;
	
	public ComputeCFCM(String propertyFile) {
		try {
			long start = System.currentTimeMillis();
			
			InputStream propsFile = new FileInputStream(propertyFile);
			this.props = new Properties();
			props.load(propsFile);
			this.urlRef = new File(props.getProperty(URL_REF)).toURI().toURL();
			this.urlResult = new File(props.getProperty(URL_RESULT)).toURI().toURL();
			this.featuresRef = new ShapeIO().readPolygonFeatures(urlRef, props.getProperty(ATTRIBUTE_REF));
			this.featuresResult = new ShapeIO().readPolygonFeatures(urlResult, props.getProperty(ATTRIBUTE_RESULT));
			this.loadBuffers = props.getProperty(LOAD_BUFFERS) != null && props.getProperty(LOAD_BUFFERS).compareTo("on")==0 ? true : false;
			
			ClassConfiguration classConfig = new ClassConfiguration();
			
			Collection<String> classPairKeys = getClassPairKeys(props);
			for (String key : classPairKeys) {
				String classId0 = key.split(",")[0];
				String classId1 = key.split(",")[1];
				if (props.containsKey(key) && props.getProperty(key).length() > 0) {
					String widthString = props.getProperty(key);
					double width = Double.parseDouble(widthString);
					classConfig.setBufferSize(classId0, classId1, width);
					LOG.info("buffer width for classes '" +classId0 + "' and '" + classId1 + "' = " + width);
				} else  {
					LOG.error("no buffer width for class combination " + key + " found.");
					LOG.info("setting width to " + Buffer.DEFAULT_BUFFER_SIZE);
				}
			}
			
			String outputPath = getPath(OUTPUT_PATH);
			
			if (cached) {
				int numPointsX = Integer.parseInt(props.getProperty(NUM_POINTS_X));
				int numPointsY = Integer.parseInt(props.getProperty(NUM_POINTS_Y));
				this.layerRef = new CachedFeatureLayer(featuresRef, classConfig, numPointsX, numPointsY);
				this.layerResult = new CachedFeatureLayer(featuresResult, classConfig, numPointsX, numPointsY);
			} else {
				this.layerRef = new FeatureLayer(featuresRef, classConfig);
				this.layerResult = new FeatureLayer(featuresResult, classConfig);
			}
			
			boolean buffersExist = buffersExist(outputPath, layerRef, layerResult);
			if (loadBuffers && buffersExist) {
				// buffers already exist
				loadBuffersCreateTransitionZones(layerRef, outputPath, props.getProperty(OUTPUT_BUFFERS_REF));
				loadBuffersCreateTransitionZones(layerResult, outputPath, props.getProperty(OUTPUT_BUFFERS_RESULT));
			} else { 
				layerRef.createBuffers();
				layerRef.createTransitionZones();
				layerResult.createBuffers();
				layerResult.createTransitionZones();
				if (!new File(outputPath).exists()) {
					boolean success = new File(outputPath).getAbsoluteFile().mkdirs();
					LOG.info("creating " + outputPath + ": " + success);
				} else {
					LOG.debug("output directory " + outputPath + " already exists.");
				}
				
				writeSHPOutput(outputPath);
			}
			
			if (cached) {
				((CachedFeatureLayer)layerRef).writeBuffersInCache();
				((CachedFeatureLayer)layerResult).writeBuffersInCache();
				ImageIO.write(((CachedFeatureLayer)layerRef).getRasters().get("A"), "png", new File("C:/temp/refA.png"));
				ImageIO.write(((CachedFeatureLayer)layerRef).getRasters().get("B"), "png", new File("C:/temp/refB.png"));
				ImageIO.write(((CachedFeatureLayer)layerResult).getRasters().get("A"), "png", new File("C:/temp/resultA.png"));
				ImageIO.write(((CachedFeatureLayer)layerResult).getRasters().get("B"), "png", new File("C:/temp/resultB.png"));
			}
			
			CFCM cfcm = new CFCM(layerRef, layerResult);
			writeTextOutput(outputPath, cfcm);
			long timeInSec = System.currentTimeMillis() - start;
			LOG.info("... finished. Took " + timeInSec / 1000 + " s.");
		} catch (MalformedURLException e) {
			LOG.error(e, e);
		} catch (IOException e) {
			LOG.error(e, e);
		}
	}

	private void loadBuffersCreateTransitionZones(FeatureLayer layer, String outputPath, String outputPathBuffers) throws MalformedURLException,
			IOException {
		Map<String, Buffer> buffersByObjId = new HashMap<String, Buffer>();
		for (String className : layer.getClassNames()) {
			String outputBuffersRef = outputPathBuffers.replace(CLASSNAME_TOKEN, className);
			URL refBuffersURL = new File(outputPath + outputBuffersRef).toURI().toURL();
			buffersByObjId.putAll(new ShapeIO().readBuffers(refBuffersURL, "id"));
		}
		for (String objId : buffersByObjId.keySet()) {
			String className = layer.getFeature(objId).getClassName();
			layer.addTransitionZone(className, new TransitionZone(objId, buffersByObjId.get(objId)));
		}
	}

	private boolean buffersExist(String outputPath, FeatureLayer layerRef, FeatureLayer layerResult) {
		for (String className : layerRef.getClassNames()) {
			String outputBuffersRef = props.getProperty(OUTPUT_BUFFERS_REF).replace(CLASSNAME_TOKEN, className);
			String outputBuffersResult = props.getProperty(OUTPUT_BUFFERS_RESULT).replace(CLASSNAME_TOKEN, className);
			File refFile = new File(outputPath + outputBuffersRef);
			File resultFile = new File(outputPath + outputBuffersResult);
			if (!refFile.exists() || !resultFile.exists()) {
				return false;
			}
		}
		return true;
	}

	private Collection<String> getClassPairKeys(Properties props2) {
		Collection<String> keys = new HashSet<String>();
		for (Object key : props.keySet()) {
			String keyName = (String) key;
			if (keyName.contains(",")) {
				keys.add(keyName);
			}
		}
		return keys;
	}

	private void writeSHPOutput(String outputPath) throws IOException,
			MalformedURLException {
		for (String className : layerRef.getClassNames()) {
			String outputBuffersRef = props.getProperty(OUTPUT_BUFFERS_REF).replace(CLASSNAME_TOKEN, className);
			String outputBuffersResult = props.getProperty(OUTPUT_BUFFERS_RESULT).replace(CLASSNAME_TOKEN, className);
			Collection<Feature> buffersRef = layerRef.getBuffersAsFeatures(className);
			Collection<Feature> buffersResult = layerResult.getBuffersAsFeatures(className);
			new ShapeIO().writeFeatures(buffersRef, PolygonFeature.SIMPLE_FEATURE_TYPE, new File(outputPath + outputBuffersRef).toURI().toURL());
			new ShapeIO().writeFeatures(buffersResult, PolygonFeature.SIMPLE_FEATURE_TYPE, new File(outputPath + outputBuffersResult).toURI().toURL());
		}
	}

	private void writeTextOutput(String outputPath, CFCM cfcm)
			throws MalformedURLException, IOException {
		String cfcmString = "";
		int numPointsX = Integer.parseInt(props.getProperty(NUM_POINTS_X));
		int numPointsY = Integer.parseInt(props.getProperty(NUM_POINTS_Y));
		for (String className : layerRef.getClassNames()) {
			String outputCFCM = props.getProperty(OUTPUT_CFCM_POINTS).replace(CLASSNAME_TOKEN, className);
			URL outUrlCFCMPoints = new File(outputPath + outputCFCM).toURI().toURL();
			cfcm.computeCFCMPoints(className, numPointsX, numPointsY);
			double min = new Min().evaluate(cfcm.getValues());
			double max = new Max().evaluate(cfcm.getValues());
			double mean = new Mean().evaluate(cfcm.getValues());
			double stddev = new StandardDeviation().evaluate(cfcm.getValues());
			cfcmString += "cfcm('"+className+"')\t" 
			+ CFCM_FORMAT.format(min) + "\t"
			+ CFCM_FORMAT.format(max) + "\t"
			+ CFCM_FORMAT.format(mean) + "\t"
			+ CFCM_FORMAT.format(stddev) + "\n";
			LOG.info("cfcm('"+className+"')=" + CFCM_FORMAT.format(mean));
			Collection<Feature> cfcmPoints = cfcm.getCFCMPoints();
			new ShapeIO().writeFeatures(cfcmPoints, PointFeature.SIMPLE_FEATURE_TYPE, outUrlCFCMPoints);
		}
		URL outputCfcmUrl = new File(outputPath + getPath(OUTPUT_CFCM_RESULTS)).toURI().toURL();
		new TextIO().writeString(cfcmString, outputCfcmUrl);
	}

	private String getPath(String pathKey) {
		return props.getProperty(pathKey).replace("\\", "/");
	}

//	private static void checkPoint(FeatureLayer layer, String objClass, double x, double y) {
//		LOG.info("membership of class '"+objClass+"' ("+x+","+y+") == " + layer.getMembership(objClass, x, y));
//	}
	
	public static void main(String[] args) {
		new ComputeCFCM(args[0]);
	}
}
